package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.Contract.BaseContract;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * {@link Contract} base implementation that works by declaring witch annotations should be
 * processed and how each annotation modifies {@link MethodMetadata}
 */
public abstract class DeclarativeContract extends BaseContract {

    private final List<GuardedAnnotationProcessor> classAnnotationProcessors = new ArrayList<>();
    private final List<GuardedAnnotationProcessor> methodAnnotationProcessors = new ArrayList<>();
    private final Map<Class<Annotation>, DeclarativeContract.ParameterAnnotationProcessor<Annotation>> parameterAnnotationProcessors = new HashMap<>();

    @Override
    public final List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
        // any implementations must register processors
        return super.parseAndValidateMetadata(targetType);
    }

    /**
     * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target type
     * (unless they are the same).
     *
     * @param data       metadata collected so far relating to the current java method.
     * @param targetType the class to process
     */
    @Override
    protected final void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) {
        final List<GuardedAnnotationProcessor> processors = Arrays.stream(targetType.getAnnotations()).flatMap(annotation -> classAnnotationProcessors.stream().filter(processor -> processor.test(annotation))).collect(Collectors.toList());

        if (!processors.isEmpty()) {
            Arrays.stream(targetType.getAnnotations()).forEach(annotation -> processors.stream().filter(processor -> processor.test(annotation)).forEach(processor -> processor.process(annotation, data)));
        } else {
            if (targetType.getAnnotations().length == 0) {
                data.addWarning(String.format("Class %s has no annotations, it may affect contract %s", targetType.getSimpleName(), getClass().getSimpleName()));
            } else {
                data.addWarning(String.format("Class %s has annotations %s that are not used by contract %s", targetType.getSimpleName(), Arrays.stream(targetType.getAnnotations()).map(annotation -> annotation.annotationType().getSimpleName()).collect(Collectors.toList()), getClass().getSimpleName()));
            }
        }
    }

    /**
     * @param data       metadata collected so far relating to the current java method.
     * @param annotation annotations present on the current method annotation.
     * @param method     method currently being processed.
     */
    @Override
    protected final void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
        List<GuardedAnnotationProcessor> processors = methodAnnotationProcessors.stream().filter(processor -> processor.test(annotation)).collect(Collectors.toList());

        if (!processors.isEmpty()) {
            processors.forEach(processor -> processor.process(annotation, data));
        } else {
            data.addWarning(String.format("Method %s has an annotation %s that is not used by contract %s", method.getName(), annotation.annotationType().getSimpleName(), getClass().getSimpleName()));
        }
    }

    /**
     * @param data        metadata collected so far relating to the current java method.
     * @param annotations annotations present on the current parameter annotation.
     * @param paramIndex  if you find a name in {@code annotations}, call {@link
     *                    #nameParam(MethodMetadata, String, int)} with this as the last parameter.
     * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
     * http-relevant annotation.
     */
    @Override
    protected final boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
        List<Annotation> matchingAnnotations = Arrays.stream(annotations).filter(annotation -> parameterAnnotationProcessors.containsKey(annotation.annotationType())).collect(Collectors.toList());

        if (!matchingAnnotations.isEmpty()) {
            matchingAnnotations.forEach(annotation -> parameterAnnotationProcessors.getOrDefault(annotation.annotationType(), ParameterAnnotationProcessor.DO_NOTHING).process(annotation, data, paramIndex));

        } else {
            final Parameter parameter = data.method().getParameters()[paramIndex];
            String parameterName = parameter.isNamePresent() ? parameter.getName() : parameter.getType().getSimpleName();
            if (annotations.length == 0) {
                data.addWarning(String.format("Parameter %s has no annotations, it may affect contract %s", parameterName, getClass().getSimpleName()));
            } else {
                data.addWarning(String.format("Parameter %s has annotations %s that are not used by contract %s", parameterName, Arrays.stream(annotations).map(annotation -> annotation.annotationType().getSimpleName()).collect(Collectors.toList()), getClass().getSimpleName()));
            }
        }
        return false;
    }

    /**
     * Called while class annotations are being processed
     *
     * @param annotationType to be processed
     * @param processor      function that defines the annotations modifies {@link MethodMetadata}
     */
    protected <E extends Annotation> void registerClassAnnotation(Class<E> annotationType, DeclarativeContract.AnnotationProcessor<E> processor) {
        registerClassAnnotation(annotation -> annotation.annotationType().equals(annotationType), processor);
    }

    /**
     * Called while class annotations are being processed
     *
     * @param predicate to check if the annotation should be processed or not
     * @param processor function that defines the annotations modifies {@link MethodMetadata}
     */
    protected <E extends Annotation> void registerClassAnnotation(Predicate<E> predicate, DeclarativeContract.AnnotationProcessor<E> processor) {
        this.classAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
    }

    /**
     * Called while method annotations are being processed
     *
     * @param annotationType to be processed
     * @param processor      function that defines the annotations modifies {@link MethodMetadata}
     */
    protected <E extends Annotation> void registerMethodAnnotation(Class<E> annotationType, DeclarativeContract.AnnotationProcessor<E> processor) {
        registerMethodAnnotation(annotation -> annotation.annotationType().equals(annotationType), processor);
    }

    /**
     * Called while method annotations are being processed
     *
     * @param predicate to check if the annotation should be processed or not
     * @param processor function that defines the annotations modifies {@link MethodMetadata}
     */
    protected <E extends Annotation> void registerMethodAnnotation(Predicate<E> predicate, DeclarativeContract.AnnotationProcessor<E> processor) {
        this.methodAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
    }

    /**
     * Called while method parameter annotations are being processed
     *
     * @param annotation to be processed
     * @param processor  function that defines the annotations modifies {@link MethodMetadata}
     */
    protected <E extends Annotation> void registerParameterAnnotation(Class<E> annotation, DeclarativeContract.ParameterAnnotationProcessor<E> processor) {
        this.parameterAnnotationProcessors.put((Class) annotation, (DeclarativeContract.ParameterAnnotationProcessor) processor);
    }

    @FunctionalInterface
    public interface AnnotationProcessor<E extends Annotation> {

        /**
         * @param annotation present on the current element.
         * @param metadata   collected so far relating to the current java method.
         */
        void process(E annotation, MethodMetadata metadata);
    }

    @FunctionalInterface
    public interface ParameterAnnotationProcessor<E extends Annotation> {

        DeclarativeContract.ParameterAnnotationProcessor<Annotation> DO_NOTHING = (ann, data, i) -> {
        };

        /**
         * @param annotation present on the current parameter annotation.
         * @param metadata   metadata collected so far relating to the current java method.
         * @param paramIndex if you find a name in {@code annotations}, call {@link
         *                   #nameParam(MethodMetadata, String, int)} with this as the last parameter.
         * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
         * http-relevant annotation.
         */
        void process(E annotation, MethodMetadata metadata, int paramIndex);
    }

    private class GuardedAnnotationProcessor implements Predicate<Annotation>, DeclarativeContract.AnnotationProcessor<Annotation> {

        private final Predicate<Annotation> predicate;
        private final DeclarativeContract.AnnotationProcessor<Annotation> processor;

        @SuppressWarnings({"rawtypes", "unchecked"})
        private GuardedAnnotationProcessor(Predicate predicate, DeclarativeContract.AnnotationProcessor processor) {
            this.predicate = predicate;
            this.processor = processor;
        }

        @Override
        public void process(Annotation annotation, MethodMetadata metadata) {
            processor.process(annotation, metadata);
        }

        @Override
        public boolean test(Annotation t) {
            return predicate.test(t);
        }
    }
}
